﻿using System;
using System.Linq;
using System.Reflection;

namespace Visitor {
	public interface IVisitor<T> { }
	public interface IVisitor<T, T2> : IVisitor<T> where T2 : T {
		void Visit(T2 visitable);
	}

	public interface IVisitorR<T> : IVisitor<T> { }
	public interface IVisitorR<T, R> : IVisitorR<T> { }
	public interface IVisitorR<T, T2, R> : IVisitorR<T, R> where T2 : T {
		R Visit(T2 visitable);
	}

	public static class VisitExt {
		private static MethodInfo GetMethod(object visitor, object self) {
			return visitor.GetType().GetMethod("Visit", new[] { self.GetType() });
		}

		private static MethodInfo GetMethod<T>(object visitor, object self) {
			var f = (from m in visitor.GetType().GetMethods()
							 where m.GetParameters().Length == 1
										 && m.GetParameters()[0].ParameterType == self.GetType()
										 && m.ReturnType == typeof(T)
										 && m.Name == "Visit"
							 select m).FirstOrDefault();
			return f;
		}

		public static void Accept<T, T2>(this T self, IVisitor<T, T2> visitor) where T2 : T {
			var method = GetMethod(visitor, self);
			if (method == null)
				throw new ArgumentException(string.Format("Visit() not found for visitor '{0}' type '{1}'", visitor.GetType(),
																									self.GetType()));
			method.Invoke(visitor, new object[] { self });
		}

		public static void Accept(this object self, object visitor) {
			var method = GetMethod(visitor, self);
			if (method == null)
				throw new ArgumentException(string.Format("Visit() not found for visitor '{0}' type '{1}'", visitor.GetType(),
																									self.GetType()));
			method.Invoke(visitor, new[] { self });
		}

		public static void Accept<T>(this T self, object visitor, Action defaultAction) {
			var method = GetMethod(visitor, self);
			if (method == null)
				defaultAction.Invoke();
			else
				method.Invoke(visitor, new object[] { self });
		}

		public static void Accept<T>(this T self, object visitor, Action<T> defaultAction) {
			var method = GetMethod(visitor, self);
			if (method == null)
				defaultAction.Invoke(self);
			else
				method.Invoke(visitor, new object[] { self });
		}

		/* Returning visitors */

		public static R Accept<R>(this object self, object visitor) {
			var method = GetMethod<R>(visitor, self);
			if (method == null)
				throw new ArgumentException(string.Format("Visit() not found for visitor '{0}' type '{1}'", visitor.GetType(),
																									self.GetType()));
			return (R)method.Invoke(visitor, new[] { self });
		}

		public static R Accept<T, R>(this T self, IVisitorR<T, R> visitor) {
			return Accept<R>(self, visitor);
		}

		public static R Accept<T, T2, R>(this T self, IVisitorR<T, T2, R> visitor) where T2 : T {
			return Accept<R>(self, visitor);
		}

		public static R Accept<R>(this object self, object visitor, R defaultValue) {
			var method = GetMethod<R>(visitor, self);
			if (method == null)
				return defaultValue;
			return (R)method.Invoke(visitor, new[] { self });
		}

		public static R Accept<T, R>(this T self, object visitor, Func<T, R> defaultValue) {
			var method = GetMethod<R>(visitor, self);
			if (method == null)
				return defaultValue.Invoke(self);
			return (R)method.Invoke(visitor, new object[] { self });
		}

		public static R Accept<T, R>(this T self, object visitor, Func<R> defaultValue) {
			var method = GetMethod<R>(visitor, self);
			if (method == null)
				return defaultValue.Invoke();
			return (R)method.Invoke(visitor, new object[] { self });
		}
	}
}